<template>
  <div>
    <input type="file" ref="fileInput" @change="handleFileChange" />
    <button @click="startFileUpload">开始上传</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';

export default defineComponent({
  data() {
    return {
      file: null as File | null,
      identifier: '',
      chunkSize: 1024 * 4, // 1MB 分片大小，用于将文件切割成合适大小的分片进行上传
      resumePosition: 0, // 默认从头开始上传，记录下次开始上传的分片位置
    };
  },
  methods: {
    /**
     * 处理文件选择框内容变化的方法，当用户选择了文件后，获取选中的文件并赋值给this.file。
     * @param event 触发的事件对象，包含了文件选择框相关的信息
     */
    handleFileChange(event:any) {
      const target = event.target as HTMLInputElement;
      if (target.files && target.files.length > 0) {
        this.file = target.files[0];
      }
    },

    /**
     * 启动文件上传的主方法，首先检查是否有选中的文件，然后生成文件上传的唯一标识符，
     * 根据文件大小和设定的分片大小计算总分片数，接着循环上传每个分片，最后提示文件上传完成。
     * 如果中途出现分片上传失败会抛出错误并在控制台打印错误信息。
     */
    async startFileUpload() {
      if (!this.file) return;
      this.identifier = this.generateUploadIdentifier(); // 生成用于标识此次文件上传的唯一标识符
      const totalChunks = Math.ceil(this.file.size / this.chunkSize);
      for (let chunkNumber = this.resumePosition; chunkNumber < totalChunks; chunkNumber++) {
        const startByte = chunkNumber * this.chunkSize;
        const endByte = Math.min(startByte + this.chunkSize, this.file.size);
        const chunk = this.file.slice(startByte, endByte);
        await this.uploadFileChunk(chunk, chunkNumber, totalChunks, this.identifier);
      }
      alert('文件上传完成');
    },

    /**
     * 上传单个文件分片的方法，将分片数据以及相关的上传参数（分片编号、总分片数、文件标识符）整理成FormData格式，
     * 然后通过axios发送POST请求到指定的文件上传接口，若请求过程出现错误会在控制台打印错误信息并抛出错误，
     * 以便外层函数进行相应处理。
     * @param chunk 要上传的文件分片数据，类型为Blob
     * @param chunkNumber 当前分片的编号，从0开始计数，类型为number
     * @param totalChunks 文件总共的分片数量，类型为number
     * @param identifier 用于唯一标识该文件上传任务的字符串，类型为string
     */
    async uploadFileChunk(chunk: Blob, chunkNumber: number, totalChunks: number, identifier: string) {
      const formData = new FormData();
      formData.append('file', new File([chunk], this.file?.name || '')); // 设置文件名为this.file.name，确保上传的分片使用原文件的名称
      formData.append('chunkNumber', chunkNumber.toString()); // 将数字类型的分片编号转换为字符串类型，符合接口参数要求
      formData.append('totalChunks', totalChunks.toString()); // 将总分片数转换为字符串类型，符合接口参数要求
      formData.append('identifier', identifier);

      try {
        await axios.post('/api/upload', formData, {
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        });
      } catch (error) {
        console.error('分片上传失败', error);
        throw error;
      }
    },

    /**
     * 生成文件上传标识符的方法，目前简单地使用当前时间戳作为标识符，
     * 在实际应用中可考虑使用更专业的uuid库来生成唯一、通用且不易重复的标识符，以确保不同上传任务能准确区分。
     * @returns 返回生成的文件上传标识符，类型为string
     */
    generateUploadIdentifier() {
      return Date.now().toString();
    },

    /**
     * 获取文件上传的继续位置（即下一次应该从哪个分片开始上传）的方法，通过向服务器发送GET请求，
     * 传递文件的标识符作为参数，获取服务器返回的继续位置数据，若出现请求错误会在控制台打印错误信息并抛出错误，
     * 便于组件进行相应处理，比如重新尝试获取等操作。成功获取后会更新组件内记录的继续位置信息（this.resumePosition）。
     */
    async getUploadResumePosition() {
      try {
        const response = await axios.get('/api/resume', {
          params: {
            identifier: this.identifier,
          },
        });
        this.resumePosition = response.data; // 更新继续位置，用于下次上传时从正确的分片开始
      } catch (error) {
        console.error('获取继续位置失败', error);
        throw error;
      }
    },
  },
  async mounted() {
    // 在组件挂载（初始化加载）时，调用获取文件上传继续位置的方法，以便在页面刷新等情况时能接着上次的进度上传
    await this.getUploadResumePosition();
  },
});
</script>